<?php

require_once 'util.php';

// TODO: Input should also preprocess things, like JSON, for the actions
class Input {
	
	private $name;
	private $alias;
	// Must have a value field: e.g. the action name is set as soon as this input is created.
	private $value;
	private $checks = array(); // Array of check objects
	
	public function __construct($name=null, $alias=null, $check=null) {
		assert(!is_array($alias)); // Could be mistaken for a list of checks
		$this->name = $name;
		$this->alias = $alias;
		if ($check != null) {
			$this->checks = is_array($check) ? $check : array($check);
		}
	}
	
	public function get_name() {
		return $this->name;
	}
	
	public function get_default_alias() {
		return $this->alias;
	}
	
	public function get_checks() {
		return $this->checks;
	}
	
	public function add_check($check) {
		assert($check instanceof Check);
		$this->checks[] = $check;
	}
	
	public function set_value($value) {
		$this->value = $value;
	}
	
	public function get_value() {
		return $this->value;
	}
	
	public function validate($value) {
		
		$result = $this->preprocess($value);
		if (!$result['valid']) {
			return $result;
		}
		else {
			$value = $result['preprocessed'];
		}
	
		foreach ($this->checks as $check) {
			
			try {
				$valid = $check->validate_php($value);
			}
			catch (Exception $e) {
				if (defined('DEBUG') and DEBUG) { 
					throw $e;
				}
				$valid = false;
			}
			
			if (!$valid) {
				$message = $check->comment($this->alias);
				
				if (defined('DEBUG') and DEBUG) {
					$check_name = get_class($check);
					$value = json_encode($value);
					$message .= "
						<div>
							<h3>[DEBUG] Validation failed</h3> 
							<p>Received value: $value</p>
							<p>Failing_check: $check_name</p>
						</div>
					";
				}
				return array('valid' => false, 'message' => $message);
			}
		}
		
		return array('valid' => true, 'message' => 'All ok');
		
	}
	
	public function preprocess($value) {
		return array('valid' => true, 'preprocessed' => $value);
	}
	
}


// TODO: Checks that are JSON aware should inherit from a special JsonCheck class, as a marker.
// JsonInput can check for this
class JsonInput extends Input {
	
	public function preprocess($value) {
		if (is_json($value)) {
			
			return array(
				'valid' => true, 
				'preprocessed' => json_decode($value, true)
			);
			
		} else {
			
			return array(
				'valid' => false, 
				'message' => "The provided string for {$this->get_default_alias()} ('$value') is not JSON."
			);
			
		}
	}
	
}


abstract class ClientSideCheck {
	
	/**
	 * Comment describing why the input is invalid. 
	 * Serves as documentation of the Check, too.
	 * 
	 * @param string $alias
	 */
	public function comment($alias) {
		return "The data for field $alias is invalid";
	}
	
	/**
	 * Return the body of a javascript function (called with $value as argument)
	 * that checks the value as it will be sent to the server.
	 * 
	 * @param string $value the name of the variable which holds the value. 
	 * @returns boolean validity
	 */
	public function validate_js($value) { return "
		return true;
	";}
	
}

abstract class Check extends ClientSideCheck {
	
	/**
	 * Checks the value for validity
	 * 
	 * @param string $value
	 * @returns boolean validity
	 */
	public abstract function validate_php($value);
	
}


class MinLengthCheck extends Check {
	
	private $min_length; 
	
	public function __construct($min_length) {
		$this->min_length = $min_length;
	}
	
	public function comment($alias) {
		return ($this->min_length == 1) ? 
					"The field $alias is empty, which is not allowed." : 
					"The field $alias is too short (length {$this->min_length} required).";
	}
	
	public function validate_php($value) {
		return strlen($value) >= $this->min_length;
	}
	
	public function validate_js($value) { return "
		return $value.length >= {$this->min_length};
	";}
	
}


class MaxLengthCheck extends Check {
	
	private $max_length; 
	
	public function __construct($max_length) {
		$this->max_length = $max_length;
	}
	
	public function comment($alias) {
		return "The field $alias is too long (length {$this->max_length} is the limit).";
	}
	
	public function validate_php($value) {
		return strlen($value) <= $this->max_length;
	}
	
	public function validate_js($value) { return "
		return $value.length <= {$this->max_length};
	";}
	
}


class TextInput extends Input {
	
	public function __construct($name=null, $alias=null, $min_length=0, $max_length=0) {
		parent::__construct($name, $alias);
		
		if ($min_length != 0) {
			$this->add_check(new MinLengthCheck($min_length));
		}
		
		if ($max_length != 0) {
			$this->add_check(new MaxLengthCheck($max_length));
		}
	}
	
}


class RegexCheck extends Check {
	
	private $regex;
	private $message;
	
	public function __construct($regex, $message) {
		$this->regex = $regex;
		$this->message = $message;
	}
	
	public function comment($alias) {
		return str_replace('$alias', $alias, $this->message);
	}
	
	public function validate_php($value) {
		return preg_match($this->regex, $value);
	}
	
	public function validate_js($value) { return "
		return {$this->regex}.test($value);
	";}
	
}


class EmailInput extends Input {
	
	public function __construct($name=null, $alias=null) {
		parent::__construct($name, $alias);
		$this->add_check(new RegexCheck(
			'/^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/',
			'$alias is not a valid email address.'
		));
	}
	
}


class JsonListCheck extends Check {
	
	public function comment($alias) {
		return "The provided JSON for $alias is not a list.";
	}
	
	public function validate_php($value) {
		return is_array($value); 
	}
	
}


class JsonListElementsCheck extends Check {
	
	private $check;
	
	public function __construct($check) {
		$this->check = $check;
	}
	
	public function comment($alias) {
		return "At least one of the given values for $alias has a problem: ". 
				$this->check->comment($alias . '>');
	}
	
	public function validate_php($value) {
		foreach ($value as $item) {
			if (!$this->check->validate_php($item)) {
				return false;
			}
		}
		return true;
	}
	
	public function validate_js($value) { return "
		function {$value}_check({$value}_subitem) {
			{$this->check->validate_js($value . '_subitem')}
		}
		for (var i=0; i<$value.length; i++) {
			if (!{$value}_check({$value}[i])) {
				return false;
			}
		}
		return true;
	";}
			
}

class JsonListMinLengthCheck extends Check {
	
	private $min_length;
	
	public function __construct($min_length) {
		$this->min_length = $min_length;
	}
	
	public function comment($alias) {
		return ($this->min_length == 1) ? 
					"The list $alias is empty, which is not allowed.": 
					"The list $alias is too short (length {$this->min_length} required).";
	}
	
	public function validate_php($value) {
		return count($value) >= $this->min_length;
	}
	
	public function validate_js($value) { return "
		return $value.length >= {$this->min_length};
	";}	
	
}


class JsonListMaxLengthCheck extends Check {
	
	private $max_length;
	
	public function __construct($max_length) {
		$this->max_length = $max_length;
	}
	
	public function comment($alias) {
		return "The list $alias is too long (length {$this->max_length} is the limit).";
	}
	
	public function validate_php($value) {
		return count($value) <= $this->max_length;
	}
	
	public function validate_js($value) { return "
		return $value.length <= {$this->max_length};
	";}	
	
}


class ListInput extends JsonInput {
	
	public function __construct($name, $alias, $listItemInput, $min_length=0, $max_length=0) {
		parent::__construct($name, $alias);
		$this->add_check(new JsonListCheck());
		foreach ($listItemInput->get_checks() as $check) { 
			$this->add_check(new JsonListElementsCheck($check));
		}
		if ($min_length != 0) {
			$this->add_check(new JsonListMinLengthCheck($min_length));
		}
		if ($max_length != 0) {
			$this->add_check(new JsonListMaxLengthCheck($max_length));
		}
	}
	
}

class JsonTupleCheck extends Check {
	
	private $inputs;
	
	public function __construct(array $inputs) {
		$this->inputs = $inputs;
	}
	
	public function comment($alias) {
		return "The data given for $alias has a problem.";
	}
	
	public function validate_php($value) {
		if (!is_array($value) || count($value) != count($this->inputs)) {
			return false;
		}
		for ($i = 0; $i < count($this->inputs); $i++) {
			$input = $this->inputs[$i];
			assert($input instanceof Input);
			$item = $value[$i];
			foreach ($input->get_checks() as $check) {
				if (!$check->validate_php($item)) {
					return false;
				}
			}
		}
		return true;
	}
	
}


class TupleInput extends JsonInput {
	// A fixed-length list
	
	private $columns; 	
	
	public function __construct($name, $alias, $column_inputs, $min_length=0, $max_length=0) {
		parent::__construct($name, $alias);
		$this->columns = $column_inputs;
		$this->add_check(new JsonTupleCheck($column_inputs));
	}
	
	private function get_columns() {
		return $this->columns;
	}
	
}


class MultiChoiceCheck extends Check {
	
	private $choices;
	
	public function __construct($choices) {
		// Choices: simple non-associative array.
		$this->choices = array_map('strval', $choices);
	}
	
	public function comment($alias) {
		return "$alias does not contain a valid choice (should be one of [".implode(',', $this->choices)."])";
	}
	
	public function validate_php($value) {
		return in_array(strval($value), $this->choices, true);
	}
	
	public function validate_js($value) { return "
		return jQuery.inArray($value, ".json_encode($this->choices).") != -1;
	";}
	
}


class MultiChoiceInput extends Input {
	
	private $choices;
	
	/**
	 * @param array $choices mapping of identifier => choice. Use choice => choice if you don't want to use identifiers.
	 */
	public function __construct($name, $alias, $choices) {
		parent::__construct($name, $alias);
		$this->choices = $choices;
		$this->add_check(new MultiChoiceCheck(array_keys($choices)));
	}
	
	public function get_choices() {
		return $this->choices;
	}
	
}


class GenderInput extends MultiChoiceInput {
	
	private static $choices = array('M' => 'Man', 'F' => 'Woman');
	
	public function __construct($name=null, $alias=null) {
		parent::__construct($name, $alias, GenderInput::$choices);
	}
	
}


class LanguageCodeInput extends MultiChoiceInput {
	
	static public $languages = array(
		'af' => 'Afrikaans (Afrikaans)',
		'ar' => 'Arabic (Arabic)',
		'bg' => 'Bulgarian (Bulgarian)',
		'ca' => 'Catalan (Catal&#0224;)',
		'zh' => 'Chinese (Chinese)',
		'hr' => 'Croatian (hrvatski)',
		'cs' => 'Czech (cesky)',
		'da' => 'Danish (dansk)',
		'nl' => 'Dutch (Nederlands)',
		'en' => 'English (English)',
		'eo' => 'Esperanto (Esperanto)',
		'et' => 'Estonian (eesti)',
		'fi' => 'Finnish (suomi)',
		'fr' => 'French (fran&#0231;ais)',
		'de' => 'German (Deutsch)',
		'el' => 'Greek, Modern (Greek)',
		'hi' => 'Hindi (Hindi)',
		'hu' => 'Hungarian (Magyar)',
		'id' => 'Indonesian (Bahasa Indonesia)',
		'it' => 'Italian (Italiano)',
		'ja' => 'Japanese (Japanese)',
		'ko' => 'Korean (Korean)',
		'lb' => 'Luxembourgish (L&#0235;tzebuergesch)',
		'no' => 'Norwegian (Norsk)',
		'fa' => 'Persian (Farsi)',
		'pl' => 'Polish (polski)',
		'pt' => 'Portuguese (Portugu&#0234;s)',
		'ro' => 'Romanian (rom&#0226;n&#0259;)',
		'ru' => 'Russian (russian)',
		'sk' => 'Slovak (slovencina)',
		'sl' => 'Slovene (sloven&#0353;cina)',
		'es' => 'Spanish (espa&#0241;ol)',
		'sv' => 'Swedish (svenska)',
		'tr' => 'Turkish (T&#0252;rk&#0231;e)',
	);
	
	public function __construct($name=null, $alias=null) {
		parent::__construct($name, $alias, LanguageCodeInput::$languages);
	}
	
	public static function get_english_language_name($code) {
		$language = LanguageCodeInput::$languages[$code];
		$bracket = strpos($language, '(');
		if ($bracket !== false) {
			$language = substr($language, 0, $bracket);
		}
		return trim($language);
	}
	
}


class FloatCheck extends Check {
	
	public function comment($alias) {
		return "$alias does not contain a valid floating-point number";
	}
	
	public function validate_php($value) {
		return is_numeric($value);
	}
	
	public function validate_js($value) { return "
		return !isNaN(Number($value));
	";}
	
}


class FloatMinCheck extends Check {
	
	private $min;
	
	public function __construct($min) {
		$this->min = $min;
	}
	
	public function comment($alias) {
		return "The field $alias is too small ({$this->min} is the limit).";
	}
	
	public function validate_php($value) {
		return $value >= $this->min;
	}
	
	public function validate_js($value) { return "
		return Number($value) >= {$this->min};
	";}
	
}


class FloatMaxCheck extends Check {
	
	private $max;
	
	public function __construct($max) {
		$this->max = $max;
	}
	
	public function comment($alias) {
		return "The field $alias is too large ({$this->max} is the limit).";
	}
	
	public function validate_php($value) {
		return $value <= $this->max;
	}
	
	public function validate_js($value) { return "
		return Number($value) <= {$this->max};
	";}
	
}


class FloatInput extends Input {
	
	public function __construct($name=null, $alias=null, $min=null, $max=null) {
		parent::__construct($name, $alias);
		$this->add_check(new FloatCheck());
		if ($min !== null) {
			$this->add_check(new FloatMinCheck($min));
		}
		if ($max !== null) {
			$this->add_check(new FloatMaxCheck($max));
		}
	}
	
}


class DateCheck extends Check {
	
	public function comment($alias) {
		return "The field $alias does not contain a date in dd/mm/yyyy format";
	}
	
	public function validate_php($value) {
		try {
			$result = slash_date_string_to_date($value);
			return true;
		} catch (Exception $e) {
			return false;
		}
	}
	
	public function validate_js($value) { return "
		exploded = jQuery.trim($value).split('/');
		if (exploded.length !== 3 || 
				exploded[0] < 1 || exploded[0] > 31 ||
				exploded[1] < 1 || exploded[1] > 12 ||
				exploded[2] < 1900 || exploded[2] > 2100) {
			return false;
		}	
		return true;
	";}
	
}


class DateInput extends Input {
	
	public function __construct($name=null, $alias=null) {
		parent::__construct($name, $alias);
		$this->add_check(new DateCheck());
	}
}


class BooleanCheck extends Check {
	
	public function comment($alias) {
		"$alias should be a 0 or a 1";
	}

	public function validate_php($value) {
		return $value == "0" or $value == "1";
	}

	public function validate_js($value) { return "
		return $value == '0' || $value == '1';
	";}
	
}


class BooleanInput extends Input {
	
	public function __construct($name=null, $alias=null) {
		parent::__construct($name, $alias);
		$this->add_check(new BooleanCheck());
	}
	
}

